/*
This file is part of IRCaBot.
IRCaBot is IRC logger with features.
Source code: https://notabug.org/acetone/ircabot.
Copyright (C) acetone, 2023.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "voicegate.h"
#include "currenttime.h"
#include "zerostoragecaptcha.h"
#include "config.h"

#include <QDebug>
#include <QThread>

constexpr const uint8_t BAD_CAPTCHA_THRESHOLD_TO_DELETE = 5; // counter
constexpr const uint8_t INVITE_TIMEOUT = 120; // secs
constexpr const uint8_t TOKEN_GENERATION_DELAY_AGE_THRESHOLD = INVITE_TIMEOUT / 4;
constexpr const uint8_t TOKEN_GENERATION_DELAY = 2; // a little DoS protection

QMutex VoiceGate::m_captchaCacheMutex;
QList<QSharedPointer<VoiceGate::Captcha>> VoiceGate::m_captchaCache;

/*
 * S I G N A T U R E S
 * Invite token: NETWORK: NICKNAME: TIMESTAMP
 * Bad captcha token: ZSC TOKEN: TIMESTAMP: CURRENT BAD COUNTER
 * CaptchaView token: NETWORK: NICKNAME: ZSC TOKEN
 */

QString VoiceGate::getInviteToken(const Target &target)
{
    return Token(QStringList() << target.network << target.nickname << QString::number(currentTimestampSecs()) ).token();
}

QSharedPointer<VoiceGate::CaptchaView> VoiceGate::getCaptcha(const QString &inviteToken,  bool* timedout, QString* answer)
{
    if (timedout != nullptr) *timedout = false;

    Token invite(inviteToken);
    if (not invite.valid())
    {
        return nullptr;
    }

    const QStringList invitePayloadList = invite.payload();
    if (invitePayloadList.size() != 3)
    {
        qDebug() << "Received invalid token in VoiceGate::getCaptcha (wrong size). Rejected.";
        return nullptr;
    }

    const qint64 now = currentTimestampSecs();

    const qint64 timestamp = invitePayloadList.at(2).toLongLong();
    const qint64 tokenAge = now - timestamp;
    if (tokenAge > INVITE_TIMEOUT)
    {
        if (timedout != nullptr) *timedout = true;
        qDebug() << "Received invalid token in VoiceGate::getCaptcha (timed out). Rejected.";
        return nullptr;
    }
    else if (tokenAge > TOKEN_GENERATION_DELAY_AGE_THRESHOLD)
    {
        QThread::sleep(TOKEN_GENERATION_DELAY);
        if (tokenAge > INVITE_TIMEOUT)
        {
            if (timedout != nullptr) *timedout = true;
            qDebug() << "Received invalid token in VoiceGate::getCaptcha (timed out). Rejected after TOKEN_GENERATION_DELAY (" << TOKEN_GENERATION_DELAY <<").";
            return nullptr;
        }
    }

    Target target;
    target.network = invitePayloadList.at(0);
    target.nickname = invitePayloadList.at(1);

    QSharedPointer<CaptchaView> captchaview (new CaptchaView);
    auto cachedCaptcha = getCachedCaptcha();
    if (cachedCaptcha)
    {
        qDebug() << "Cached captcha exists. Without generation a new one! Cache size:" << m_captchaCache.size();
        if (answer) *answer = cachedCaptcha->answer;
        captchaview->init(cachedCaptcha, target, cachedCaptcha->badReportCounter);
        return captchaview;
    }

    ZeroStorageCaptcha zsc;
    zsc.generateAnswer();
    zsc.render();

    QSharedPointer<Captcha> newCaptcha (new Captcha);
    newCaptcha->answer = zsc.answer();
    newCaptcha->token = zsc.token();
    newCaptcha->timestamp = now;
    newCaptcha->imageB64 = zsc.picturePng().toBase64();

    if (answer) *answer = newCaptcha->answer;

    captchaview->init(newCaptcha, target, newCaptcha->badReportCounter);

    m_captchaCacheMutex.lock();
    if (m_captchaCache.size() < Config::voiceGate().captchaCacheSize)
    {
        m_captchaCache.push_back(newCaptcha);
    }
    qDebug() << "Captcha cache size:" << m_captchaCache.size();
    m_captchaCacheMutex.unlock();
    return captchaview;
}

void VoiceGate::badCaptchaReport(const QString &badTokenB64)
{
    const Token badToken(badTokenB64);
    if (not badToken.valid())
    {
        return;
    }

    const QStringList payloadList = badToken.payload();
    if (payloadList.size() != 3)
    {
        qDebug() << "Received invalid token in VoiceGate::badCaptchaReport (wrong size). Rejected.";
        return;
    }

    qint64 timestamp = payloadList.at(1).toLongLong();
    if (currentTimestampSecs() - timestamp > ZSC_CAPTCHA_LONGEST_TIMEOUT)
    {
        qDebug() << "Received invalid token in VoiceGate::badCaptchaReport (timed out). Rejected.";
        return;
    }

    uint8_t passedBadCounter = payloadList.at(2).toUShort();
    QMutexLocker lock (&m_captchaCacheMutex);

    const QString captchaToken = payloadList.at(0);
    int removeIndex = 0;
    bool finded = false;
    for (auto iter = m_captchaCache.begin(); iter != m_captchaCache.end(); iter++, removeIndex++)
    {
        if (iter->data()->token == captchaToken)
        {
            if (iter->data()->badReportCounter != passedBadCounter)
            {
                qDebug() << "Received invalid token in VoiceGate::badCaptchaReport (wrong bad counter passed). Rejected.";
                return;
            }

            finded = true;
            if (BAD_CAPTCHA_THRESHOLD_TO_DELETE <= ++(iter->data()->badReportCounter))
            {
                qInfo() << "Captcha with answer" << iter->data()->answer << "was deleted due to illegibility";
                m_captchaCache.removeAt(removeIndex);
                return;
            }
            else
            {
                qInfo().noquote() << "Captcha with answer" << "\"" + iter->data()->answer +  "\"" << "marked as illegibility:"
                                  << static_cast<int>(iter->data()->badReportCounter) << "/" << static_cast<int>(BAD_CAPTCHA_THRESHOLD_TO_DELETE);
            }
        }
    }
    if (not finded)
    {
        qWarning() << "VoiceGate::badCaptchaReport receive correct token, but captcha for processing not found (it's normal if cache is full or disabled).";
    }
}

QSharedPointer<VoiceGate::Target> VoiceGate::verify(const QString &captchaToken, const QString &answer)
{
    Token token(captchaToken);
    if (not token.valid())
    {
        return nullptr;
    }

    QStringList payloadList = token.payload();
    if (payloadList.size() != 3)
    {
        qDebug() << "Received invalid token in VoiceGate::verify (wrong size). Rejected.";
        return nullptr;
    }

    const QString zscToken =  payloadList.at(2);
    if (not ZeroStorageCaptcha::validate(answer, zscToken))
    {
        qDebug() << "Received invalid answer in VoiceGate::verify. Rejected.";
        return nullptr;
    }

    QSharedPointer<Target> target (new Target);
    target->network = payloadList.at(0);
    target->nickname = payloadList.at(1);

    qInfo().noquote() << "["+target->network+"]" << target->nickname << "added to VoiceGate database";

    m_captchaCacheMutex.lock();
    int removeIndex = 0;
    for (auto iter = m_captchaCache.begin(); iter != m_captchaCache.end(); iter++, removeIndex++)
    {
        if (iter->data()->token == zscToken)
        {
            m_captchaCache.removeAt(removeIndex);
            break;
        }
    }
    m_captchaCacheMutex.unlock();

    return target;
}

void VoiceGate::dropCache()
{
    QMutexLocker lock(&m_captchaCacheMutex);
    qInfo().noquote() << "VoiceGate::dropCache called." << m_captchaCache.size() << "cached captchas dropped";
    m_captchaCache.clear();
}

QSharedPointer<VoiceGate::Captcha> VoiceGate::getCachedCaptcha()
{
    QMutexLocker lock (&m_captchaCacheMutex);
    qint64 now = currentTimestampSecs();

    for (auto iter = m_captchaCache.begin(); iter != m_captchaCache.end(); iter++)
    {
        if (now - iter->data()->timestamp > ZSC_CAPTCHA_LONGEST_TIMEOUT)
        {
            iter->data()->timestamp = now;
            iter->data()->token = ZeroStorageCaptchaService::TokenManager::get( iter->data()->answer );
            return *iter;
        }
    }
    return nullptr;
}

////////////////////////

void VoiceGate::CaptchaView::init(const QSharedPointer<Captcha> captcha, const Target &target, uint8_t currentBadCounter)
{
    badToken = Token( QStringList() << captcha->token << QString::number(captcha->timestamp) << QString::number(currentBadCounter) ).token();
    token = Token( QStringList() << target.network << target.nickname << captcha->token ).token();
    imageB64 = captcha->imageB64;
}
